ResultSetIterator.java
package org.codefilarete.stalactite.sql.result;
import javax.annotation.Nullable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.function.Function;
import org.codefilarete.tool.function.ThrowingConverter;
import org.codefilarete.tool.collection.Iterables;
import org.codefilarete.tool.collection.ReadOnlyIterator;
import org.codefilarete.tool.exception.Exceptions;
/**
* Turns a {@link ResultSet} into an {@link java.util.Iterator} with row conversion support.
* You can either give the {@link ResultSet} at construct time or set it through {@link #setResultSet(ResultSet)} to reuse
* this instance.
* <br/>
* Then you can iterate over your {@link ResultSetIterator} within a loop such as this one :
* <pre>
* ResultSetIterator<String> resultSetIterator = new ResultSetIterator<String>(resultSet) {
* @Override
* public String convert(ResultSet rs) throws SQLException {
* return rs.getString("name");
* }
* };
* while (resultSetIterator.hasNext()) {
* String next = resultSetIterator.next();
* }
* </pre>
* <strong>Due to the nature of {@link ResultSet} we can't know if a next row exists without reading it, hence {@link #hasNext()} must be called
* before each access to {@link #next()}, else you'll get {@link NoSuchElementException} on {@link #next()} call.
* </strong>
*
* @author Guillaume Mary
* @see RowIterator
* @see #convert()
* @see #convert(ResultSet)
* @see #setResultSet(ResultSet)
*/
public abstract class ResultSetIterator<T> extends ReadOnlyIterator<T> implements ThrowingConverter<ResultSet, T, SQLException> {
/**
* The read {@link ResultSet}.
* Can be null if one gave null at construction time, then one can use {@link #setResultSet(ResultSet)} to be non null before iteration
*/
@Nullable
private ResultSet resultSet;
/**
* Mimics {@link ResultSet#getRow()} method which is not supported by all databases
* @see #reset()
*/
private int rowNumber = 1;
/**
* Flag that indicates if the {@link ResultSet#next()} method of the underlying instance was called.
* Used for hasNext() since {@link ResultSet} has no sure way of saying if its iteration is started or not
* @see #reset()
*/
private boolean nextCalled = false;
private boolean hasNext = false;
/**
* Default constructor
*
* @see #setResultSet(ResultSet)
*/
public ResultSetIterator() {
}
/**
* Constructor with a given {@link ResultSet}
*
* @param resultSet a ResultSet to be wrapped in an Iterator.
*/
public ResultSetIterator(ResultSet resultSet) {
this();
this.resultSet = resultSet;
}
/**
* Change the given {@link ResultSet} at construction time. Aimed at reusing this iterator.
*
* @param resultSet
*/
public void setResultSet(ResultSet resultSet) {
this.resultSet = resultSet;
reset();
}
public ResultSet getResultSet() {
return resultSet;
}
public int getRowNumber() {
return rowNumber;
}
/**
* Called when {@link ResultSet} is set. Doesn't rewind the {@link ResultSet} to the beginning.
*/
private void reset() {
this.nextCalled = false;
this.hasNext = false;
this.rowNumber = 1;
}
/**
* Returns true if there are more rows in the ResultSet.
* @return boolean true if there are more rows
* @throws RuntimeException if an SQLException occurs.
*/
@Override
public boolean hasNext() {
try {
// Warn: operator order is very important to prevent next() to be called unnecessarily
return (hasNext = !nextCalled && resultSet.next());
} catch (SQLException e) {
throw Exceptions.asRuntimeException(e);
} finally {
nextCalled = true;
}
}
@Override
public T next() {
if (!hasNext) {
// this is necessary to be compliant with Iterator#next(..) contract
throw new NoSuchElementException();
}
try {
this.rowNumber++;
return convert(resultSet);
} catch (SQLException e) {
throw new RuntimeException("Error while reading ResultSet on row " + --rowNumber, e);
} finally {
nextCalled = false;
hasNext = false;
}
}
/**
* Collects all rows of the {@link ResultSet} set by {@link #setResultSet(ResultSet)} to a {@link List}.
* This basic implementation implies that the size of the list equals the {@link ResultSet} row count.
*
* @return a non null {@link List} containing all converted rows of the {@link ResultSet}
*/
public List<T> convert() {
return Iterables.collectToList(() -> this, Function.identity());
}
/**
* Converts current row of the ResultSet
*
* @param resultSet the ResultSet given at constructor or {@link #setResultSet}
* @return the result of the conversion
*/
@Override // only for Javadoc
public abstract T convert(ResultSet resultSet) throws SQLException;
}